﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Threading;

namespace VRacer
{
    /// <summary>
    /// Zawiera wszystkie podstawowe funkcje gry. Umożliwia zainicjalizowanie gry metoda <c>Initialize</c> co ustawia wszystkie podstawowe parametry gry (laduje dostepne trasy, w niektorych przypadkach nawiazuje polaczenie z serwerem, itp itd).
    /// Metoda <c>Start</c> rozpoczyna rotacje tur, a i zatem rozgrywke. Istnieje analogiczna metoda <c>End</c>. 
    /// 
    /// Binduje się do zdarzeń gracza by zapewnic latwa ich obsluge klasom potomnym (metody wirtualne OnPlayer[Zdarzenie]), ale tez i sama klasa zapewnia podstawowe rekacje.
    /// Np w OnPlayerMoving jest analizowany ruch gracza i w zaleznosci od sytuacji wywolywana jest odpowiednia metoda HandlePlayer[Sytuacja] (np. HandlePlayerNotEnoughtMoves) tak by klasy potomne mogly w latwy sposob zaimplementowac swoja logike rozgrywki.
    /// Klasa implementuje takze standardowe zachowania metod HandlePlayer[Sytuacja].
    /// Aktualnie wybrana trasa dostepna jest pod <c>CurrentTrack</c>, aktualnie aktywny gracz pod <c>CurrentPlayer</c>.
    /// 
    /// Rotacja tur zaimplementowana jest za pomoca nastepujacych metod:
    /// GetNextPlayerInTurn - szuka kolejnego gracza ktoremu pozostaly jeszcze jakies ruchy i zwraca null jesli sie nie udalo
    /// ContinueTurn - jesli kolejny gracz w kolejce nie istnieje wywoluje <c>StartNewTurn</c> i ponawia szukanie. Jak sie znajdzie to odejmuje mu dostepne ruchy i jest szczesliwa
    /// StartNewTurn - dodaje wszystkim graczom po ruchu i zwieksza numer kolejki
    /// </summary>
    public abstract class Game : INotifyPropertyChanged
    {
        public event EventHandler<MessageEventArgs> Message;

        private PlayerBase _activePlayer;
        private int _laps = 3;
        private ObservableCollection<IBonus> _bonuses = new ObservableCollection<IBonus>();
        private ObservableCollection<PlayerBase> _players = new ObservableCollection<PlayerBase>();
        private TracksCollection _tracks;
        private Track _track;
        private Highscores _scores = new Highscores();
        private int _currentTurn = 0;
        private GameState _state = GameState.Uninitialized;

        protected Game()
        {
            PlayerBase.Reset();
            LoadTracks();
        }

        /// <summary>
        /// Dodaje nowego gracza do gry.
        /// </summary>
        /// <param name="player">The player.</param>
        public virtual void Add(PlayerBase player)
        {
            _players.Add(player);
            player.Moved += player_Moved;
            player.Moving += player_Moving;
            player.Crashed += player_Crashed;
            player.Collision += player_Collision;
            ///TODO: Game.Add ustawianie parametrow startowych playera
        }

        protected abstract void LoadTracks();

        /// <summary>
        /// Usuwa podanego zawodnika z gry.
        /// </summary>
        /// <param name="player">The player.</param>
        public virtual void Remove(PlayerBase player)
        {
            player.Moved -= player_Moved;
            player.Moving -= player_Moving;
            player.Crashed -= player_Crashed;
            player.Collision -= player_Collision;
            _players.Remove(player);

            if (_players.Count == 0)
                PlayerBase.Reset();
        }

        #region Events

        protected virtual void OnMessage(Message message)
        {
            if (Message != null)
            {
                Message(this, new MessageEventArgs(message));
            }
        }

        #endregion

        #region Events Handlers

        void player_Crashed(object sender, PlayerCrashedEventArgs e)
        {
            OnPlayerCrashed((PlayerBase)sender, e);
        }

        void player_Moved(object sender, EventArgs e)
        {
            OnPlayerMoved((PlayerBase)sender);
        }

        void player_Moving(object sender, PlayerMovingEventArgs e)
        {
            OnPlayerMoving((PlayerBase)sender, e);
        }

        void player_Collision(object sender, PlayerCollisionEventArgs e)
        {
            OnPlayerCollision((PlayerBase)sender, e);
        }

        #endregion

        #region Player events handlers

        protected virtual void OnPlayerCollision(PlayerBase player, PlayerCollisionEventArgs e)
        {

        }

        protected virtual void OnPlayerCrashed(PlayerBase player, PlayerCrashedEventArgs e)
        {

        }

        protected virtual void OnPlayerMoved(PlayerBase player)
        {
            player.Statistics.Moves++;

            if (player.IsActive)
                ContinueTurn();
        }

        protected virtual void OnPlayerMoving(PlayerBase player, PlayerMovingEventArgs e)
        {
            if (State != GameState.Started)
            {
                e.CanMove = false;
                return;
            }

            if (!IsMovePossibleWithCurrentSpeed(player, e.NewPosition))
            {
                e.CanMove = HandlePlayerNotEnoughtSpeed(player, e.NewPosition);
                if (!e.CanMove)
                    return;
            }

            if (player.Moves < 0)
            {
                e.CanMove = HandlePlayerNotEnoughtMoves(player, e.NewPosition);
                if (!e.CanMove)
                    return;
            }



            Point finishPoint = GetFinishPoint(player, e.NewPosition);
            if (finishPoint.X != -1 && finishPoint.Y != -1)
            {
                e.CanMove = HandlePlayerCrossedFinish(player, finishPoint, e.NewPosition);
                if (!e.CanMove)
                    return;
            }

            Point startPoint = GetStartPoint(player, e.NewPosition);
            if (startPoint.X != -1 && startPoint.Y != -1)
            {
                e.CanMove = HandlePlayerCrossedStart(player, finishPoint, e.NewPosition);
                if (!e.CanMove)
                    return;
            }

            Point crashPoint = GetCollisionPoint(player, e.NewPosition);
            if (crashPoint.X != -1 && crashPoint.Y != -1)
            {
                e.CanMove = HandlePlayerCrashed(player, crashPoint, e.NewPosition);
                if (!e.CanMove)
                    return;
            }

            PlayerBase uderzony = GetPlayerOnPosition(e.NewPosition.X, e.NewPosition.Y, player);
            if (uderzony != null)
            {
                e.CanMove = HandlePlayerCollision(player, uderzony, e.NewPosition);
                if (!e.CanMove)
                    return;
            }

            foreach (var bonus in Bonuses)
            {
                if (bonus.Position.X == e.NewPosition.X && bonus.Position.Y == e.NewPosition.Y)
                {
                    HandlePlayerOnBonus(player, bonus, bonus.Position);
                    break;
                }
            }

        }

        #endregion

        #region Game validation helpers

        /// <summary>
        /// Zwraca punkt kolizji dla aktywnego gracza.
        /// </summary>
        /// <param name="destination">Cel przemieszczenia gracza. Kordynaty okreslaja polozenie na mapie.</param>
        /// <returns>(-1,-1) jesli nie ma kolizji. Kordynaty okreslaja polozenie pixela.</returns>
        public Point GetCollisionPoint(Point destination)
        {
            return GetCollisionPoint(ActivePlayer, destination);
        }

        /// <summary>
        /// Zwraca punkt kolizji dla podanego gracza.
        /// </summary>
        /// <param name="player">Gracz ktorego przemieszczenie chcemy sprawdzic</param>
        /// <param name="destination">Cel przemieszczenia gracza. Kordynaty okreslaja polozenie na mapie.</param>
        /// <returns>(-1,-1) jesli nie ma kolizji. Kordynaty okreslaja polozenie pixela.</returns>
        public Point GetCollisionPoint(PlayerBase player, Point destination)
        {
            return CurrentTrack.GetCollisionPoint(player.Position.X, player.Position.Y, destination.X, destination.Y);
        }

        public Point GetFinishPoint(PlayerBase player, Point destination)
        {
            return CurrentTrack.GetFinishCrossPoint(player.Position.X, player.Position.Y, destination.X, destination.Y);
        }

        public Point GetStartPoint(PlayerBase player, Point destination)
        {
            return CurrentTrack.GetStartCrossPoint(player.Position.X, player.Position.Y, destination.X, destination.Y);
        }

        /// <summary>
        /// Zwraca zawodnika znajdujacego sie na okreslonej pozycji
        /// </summary>
        /// <param name="X">The X.</param>
        /// <param name="Y">The Y.</param>
        /// <param name="player">The Y.</param>
        /// <returns>Zwraca <c>null</c> jesli pole jest puste.</returns>
        protected PlayerBase GetPlayerOnPosition(double x, double y, PlayerBase player)
        {
            foreach (PlayerBase pb in Players)
            {
                if (pb == player)
                    continue;

                if ((pb.Position.X == x) && (pb.Position.Y == y))
                    return pb;//crash
            }

            return null;
        }

        /// <summary>
        /// Szuka nowej pozycji startowej zawodnika po kolizji ze sciana.
        /// </summary>
        /// <param name="point">Punkt w ktorym nastapila kolizja.</param>
        /// <param name="p">Zawodnik dla ktoremu szukamy miejsca w swiecie.</param>
        /// <returns></returns>
        public Point FindPointAfterCollision(Point colPoint, PlayerBase p)
        {
            Point point = new Point();
            point.X = (colPoint.X * CurrentTrack.Height / CurrentTrack.Bitmap.PixelHeight);
            point.Y = (colPoint.Y * CurrentTrack.Width / CurrentTrack.Bitmap.PixelWidth);

            if (CurrentTrack.GetCollisionPoint(p.Position.X, p.Position.Y, (int)((int)(point.X + 0.5) * CurrentTrack.Bitmap.PixelWidth / CurrentTrack.Height), (int)((int)(point.Y + 0.5) * CurrentTrack.Bitmap.PixelHeight / CurrentTrack.Width)).X == -1)
            { point.X += 0.5; point.Y += 0.5; }
            else if (CurrentTrack.GetCollisionPoint(p.Position.X, p.Position.Y, (int)((int)(point.X) * CurrentTrack.Bitmap.PixelWidth / CurrentTrack.Height), (int)((int)(point.Y) * CurrentTrack.Bitmap.PixelHeight / CurrentTrack.Width)).X == -1)
            { ; }
            else if (CurrentTrack.GetCollisionPoint(p.Position.X, p.Position.Y, (int)((int)(point.X + 1) * CurrentTrack.Bitmap.PixelWidth / CurrentTrack.Height), (int)((int)(point.Y) * CurrentTrack.Bitmap.PixelHeight / CurrentTrack.Width)).X == -1)
            { point.X += 1; }
            else if (CurrentTrack.GetCollisionPoint(p.Position.X, p.Position.Y, (int)((int)(point.X) * CurrentTrack.Bitmap.PixelWidth / CurrentTrack.Height), (int)((int)(point.Y + 1) * CurrentTrack.Bitmap.PixelHeight / CurrentTrack.Width)).X == -1)
            { point.Y += 1; }
            else if (CurrentTrack.GetCollisionPoint(p.Position.X, p.Position.Y, (int)((int)(point.X + 1) * CurrentTrack.Bitmap.PixelWidth / CurrentTrack.Height), (int)((int)(point.Y + 1) * CurrentTrack.Bitmap.PixelHeight / CurrentTrack.Width)).X == -1)
            { point.X += 1; point.Y += 1; }
            else
            {
                ;
                //TODO: znaleźć jakis inny punkt, tak, zeby nei było Ani pojazdu ani mapki
                //throw new System.NotImplementedException();
            }
            point.X = (int)point.X;
            point.Y = (int)point.Y;
            return point;
        }

        /// <summary>
        /// Sprawdza czy przemieszczenie na podany punkt jest mozliwe z aktualna predkoscia
        /// </summary>
        /// <param name="player">Zawodnik ktorego przemieszczenie sprawdzamy</param>
        /// <param name="newPosition">Koncowa pozycja po przemieszczeniu</param>
        /// <returns>
        /// 	<c>true</c> jesli wszystko ok; w przeciwnym wypadku <c>false</c>.
        /// </returns>
        protected bool IsMovePossibleWithCurrentSpeed(PlayerBase player, Point newPosition)
        {
            Point Possible = new Point(player.Position.X + player.Speed.X, player.Position.Y + player.Speed.Y);
            if ((newPosition.X <= (Possible.X + 1)) && (newPosition.X >= (Possible.X - 1)) &&
               (newPosition.Y <= (Possible.Y + 1)) && (newPosition.Y >= (Possible.Y - 1)))
                return true;
            return false;
        }

        #endregion

        #region Game situations handlers

        /// <summary>
        /// Logika zachowania gdy zawodnik ma kolizje
        /// </summary>
        /// <param name="uderzajacy">The uderzajacy.</param>
        /// <param name="uderzony">The uderzony.</param>
        /// <param name="collisionPoint">The collision point.</param>
        /// <returns></returns>
        protected virtual bool HandlePlayerCollision(PlayerBase uderzajacy, PlayerBase uderzony, Point collisionPoint)
        {
            uderzony.Hit(uderzajacy);
            uderzajacy.Crash(uderzony.Position);
            uderzajacy.Moves--;

            return true;
        }

        /// <summary>
        /// Logika zachowania gdy zawodnik nie ma wystarczajaco duzo ruchow by wykonac ruch
        /// </summary>
        /// <param name="player">The player.</param>
        /// <param name="destinationPoint">The destination point.</param>
        /// <returns></returns>
        protected virtual bool HandlePlayerNotEnoughtMoves(PlayerBase player, Point destinationPoint)
        {
            return false;
        }

        /// <summary>
        /// Logika zachowania gdy zawodnik nie ma wystarczajacej predkosci by wykonac ruch
        /// </summary>
        /// <param name="player">The player.</param>
        /// <param name="destinationPoint">The destination point.</param>
        /// <returns></returns>
        protected virtual bool HandlePlayerNotEnoughtSpeed(PlayerBase player, Point destinationPoint)
        {
            return false;
        }

        /// <summary>
        /// Logika zachowania gdy zawodnik ma wypadek
        /// </summary>
        /// <param name="player">The player.</param>
        /// <param name="crashPoint">The crash point.</param>
        /// <param name="destinationPoint">The destination point.</param>
        /// <returns></returns>
        protected virtual bool HandlePlayerCrashed(PlayerBase player, Point crashPoint, Point destinationPoint)
        {
            Point startPoint = FindPointAfterCollision(crashPoint, player);

            player.Moves--;
            player.Crash(crashPoint, startPoint);

            player.Stop();

            if (player.IsActive)
                ContinueTurn();

            return false;
        }

        /// <summary>
        /// Logika zachowania gdy zawodnik przejechal przez mete
        /// </summary>
        /// <param name="player">The player.</param>
        /// <param name="finishPoint">The finish point.</param>
        /// <param name="destinationPoint">The destination point.</param>
        /// <returns></returns>
        protected virtual bool HandlePlayerCrossedFinish(PlayerBase player, Point finishPoint, Point destinationPoint)
        {
            player.Laps++;

            if (player.Laps > this.Laps)
            {
                this.Scores.Add(player);
                player.IsFinished = true;

                bool finished = true;
                foreach (var gamePlayer in Players)
                    finished = finished && gamePlayer.IsFinished;

                if (finished)
                    Finish();
            }

            return true;
        }

        /// <summary>
        /// Logika zachowania gdy zawodnik przejechal przez start
        /// </summary>
        /// <param name="player">The player.</param>
        /// <param name="finishPoint">The finish point.</param>
        /// <param name="destinationPoint">The destination point.</param>
        /// <returns></returns>
        protected virtual bool HandlePlayerCrossedStart(PlayerBase player, Point finishPoint, Point destinationPoint)
        {
            player.Laps--;
            return true;
        }

        /// <summary>
        /// Logika zachowania gdy zawodnik zebral bonus
        /// </summary>
        /// <param name="player">The player.</param>
        /// <param name="bonus">The bonus.</param>
        /// <param name="position">The position.</param>
        /// <returns></returns>
        protected virtual bool HandlePlayerOnBonus(PlayerBase player, IBonus bonus, Point position)
        {
            bonus.Apply(player);
            Bonuses.Remove(bonus);
            return true;
        }

        /// <summary>
        /// Logika zachowania gdy gra chce umiescic nowy bonus na mapie
        /// </summary>
        /// <param name="bonus">The bonus.</param>
        /// <returns></returns>
        protected virtual bool HandleNewBonus(IBonus bonus)
        {
            if (CurrentTrack.IsPositionFree((int)bonus.Position.X, (int)bonus.Position.Y))
                Bonuses.Add(bonus);

            return true;
        }

        #endregion

        #region Game state logic

        /// <summary>
        /// Inicjalizuje gre. Np. laczy sie z serwerem, pobiera dostepne mapy itp itd.
        /// </summary>
        public virtual void Initialize()
        {
            Scores.Clear();
            LoadTracks();

            State = GameState.Initialized;
        }

        /// <summary>
        /// Rozpoczyna gre - zaczyna pierwsza kolejke i ustawia graczy na odpowiednich pozycjach
        /// </summary>
        public virtual void Start()
        {
            if (Players.Count == 0)
                return;

            SetPlayersPositions();
            Thread.Sleep(500);

            State = GameState.Started;
            if (ActivePlayer == null)
                ContinueTurn();
        }

        /// <summary>
        /// Konczy gre.
        /// </summary>
        public virtual void End()
        {
            State = GameState.Ended;
        }

        public virtual void Finish()
        {
            State = GameState.Finished;
            CurrentTrack.Highscores.Update(Scores);
            CurrentTrack.SaveHighscores();
        }

        #endregion

        #region Turn logic

        /// <summary>
        /// Szuka nastepnego gracza w kolejce
        /// </summary>
        /// <param name="currentIndex">Index of the current.</param>
        /// <returns></returns>
        public PlayerBase GetNextPlayerInTurn(int currentIndex)
        {
            for (int i = currentIndex; i < _players.Count; i++)
                if (_players[i].Moves > 0 && !_players[i].IsFinished)
                    return _players[i];

            return null;
        }

        /// <summary>
        /// Szuka nastepnego gracza w kolejce
        /// </summary>
        /// <returns></returns>
        public PlayerBase GetNextPlayerInTurn()
        {
            int currentIndex = _activePlayer == null ? 0 : _players.IndexOf(_activePlayer);
            if (currentIndex == -1)
                currentIndex = 0;

            return GetNextPlayerInTurn(currentIndex);
        }

        /// <summary>
        /// Konczy ture bierzacego gracza i rozpoczyna ture nastepnego. W razie potrzeby rozpoczyna kolejna kolejke
        /// </summary>
        public virtual void ContinueTurn()
        {
            if (State != GameState.Initialized && State != GameState.Started)
                return;

            PlayerBase nextPlayer = GetNextPlayerInTurn();
            while (nextPlayer == null)
            {
                StartNewTurn();
                nextPlayer = GetNextPlayerInTurn(0);
            }

            ContinueTurn(nextPlayer);
        }

        /// <summary>
        /// Kontynuuje ture - ustawia nastepnego aktywnego gracza
        /// </summary>
        /// <param name="nextPlayer">The next player.</param>
        public void ContinueTurn(PlayerBase nextPlayer)
        {
            //jesli jest ActivePlayer to zakoncz jego ture
            if (ActivePlayer != null)
            {
                ActivePlayer.EndTurn();

            }

            ActivePlayer = nextPlayer;
            nextPlayer.Moves--;
            nextPlayer.StartTurn();
        }

        /// <summary>
        /// Rozpoczyna nowa ture
        /// </summary>
        protected void StartNewTurn()
        {
            foreach (var player in Players)
                player.Moves++;

            CurrentTurn++;

            if (CurrentTurn % 5 == 0)
            {
                HandleNewBonus(Bonus.BonusFactory.Get(CurrentTrack));
            }
        }

        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            try
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
            catch (Exception) { }
        }

        #endregion

        #region Properties

        /// <summary>
        /// Zwraca gracza ktory aktualnie wykonuje ruch.
        /// </summary>
        /// <value>The active player.</value>
        public PlayerBase ActivePlayer
        {
            get { return _activePlayer; }
            protected set
            {
                if (_activePlayer != value)
                {
                    _activePlayer = value;
                    OnPropertyChanged("ActivePlayer");
                }

            }
        }

        public ObservableCollection<IBonus> Bonuses
        {
            get { return _bonuses; }
        }

        public int Laps
        {
            get { return _laps; }
            set
            {
                if (_laps != value)
                {
                    _laps = value;
                    OnPropertyChanged("Laps");
                }
            }
        }

        public bool IsActive
        {
            get { return State == GameState.Started; }
        }

        public bool IsEditable
        {
            get { return State == GameState.Initialized || State == GameState.Uninitialized; }
        }

        public bool IsFinished
        {
            get { return State == GameState.Ended || State == GameState.Finished; }
        }

        /// <summary>
        /// Zwraca wszystkich grajacych zawodnikow
        /// </summary>
        /// <value>The players.</value>
        public ObservableCollection<PlayerBase> Players
        {
            get
            {
                return _players;
            }
        }

        /// <summary>
        /// Zwraca aktualna mape
        /// </summary>
        /// <value>The track.</value>
        public Track CurrentTrack
        {
            get
            {
                if (_track == null)
                    CurrentTrack = Tracks[0];

                return _track;
            }
            set
            {
                if (_track != value)
                {
                    _track = value;

                    OnPropertyChanged("CurrentTrack");
                }
            }
        }

        /// <summary>
        /// Zwraca numer bierzacej tury
        /// </summary>
        /// <value>The current turn.</value>
        public int CurrentTurn
        {
            get { return _currentTurn; }
            protected set
            {
                if (_currentTurn != value)
                {
                    _currentTurn = value;
                    OnPropertyChanged("CurrentTurn");
                }
            }
        }

        public virtual GameState State
        {
            get { return _state; }
            set
            {
                if (_state != value)
                {
                    _state = value;
                    OnPropertyChanged("State");
                    OnPropertyChanged("IsActive");
                    OnPropertyChanged("IsEditable");
                    OnPropertyChanged("IsFinished");
                }
            }
        }

        public Highscores Scores
        {
            get { return _scores; }
            protected set
            {
                _scores = value;
                OnPropertyChanged("Scores");
            }
        }

        /// <summary>
        /// Zwraca wszystkie dostepne mapy
        /// </summary>
        /// <value>The tracks.</value>
        public TracksCollection Tracks
        {
            get
            {
                if (_tracks == null)
                    LoadTracks();

                return _tracks;
            }
            set
            {
                _tracks = value;
            }
        }

        #endregion

        /// <summary>
        /// Ustawia pozycje startowe zawodnikom
        /// </summary>
        protected virtual void SetPlayersPositions()
        {
            int i = 0;

            foreach (var player in Players)
            {
                player.Position = _track.StartSlots[i];
                i = (i + 1) % _track.StartSlots.Length;
            }
        }

    }
}
